package cn.kevinkun.TableViewer;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;

import org.json.JSONArray;
import org.json.JSONException;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesAssets;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.runtime.EventDispatcher;
import com.google.appinventor.components.runtime.HVArrangement;
import com.google.appinventor.components.runtime.ReplForm;
import com.google.appinventor.components.runtime.errors.YailRuntimeError;
import com.google.appinventor.components.runtime.util.FileUtil;
import com.google.appinventor.components.runtime.util.YailList;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;

@DesignerComponent(version = 9, versionName = "9.0", category = ComponentCategory.EXTENSION, description = "Show a list of list as table view. Custom the UI with css", iconName = "aiwebres/icon.png", nonVisible = true, helpUrl = "https://kevinkun.cn/tableviewer")
@SimpleObject(external = true)
@UsesAssets(fileNames = "")
@UsesPermissions(permissionNames = "")
public class TableViewer extends AndroidNonvisibleComponent {

	private String TAG = "TableViewer";
	private Context context;
	private Activity activity;
	WebView webView;
	boolean isShowTable = false;
	boolean transparentbg = false;
//	int visibleRow = 1;
//	int visibleCol = 1;

	HVArrangement container;

	private HashMap<String, HashMap<String, String>> STYLES = new LinkedHashMap<String, HashMap<String, String>>();
	private ArrayList<ArrayList<String>> DATAS = new ArrayList<ArrayList<String>>();
	private HashMap<String, String> CLASSES = new HashMap<String, String>();

	private boolean hasHead = false;
	private boolean hasFoot = false;

	private String styleSheet = "";
	private String part1 = "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0\"name=\"viewport\"/>";
	private String part2 = "<script type=\"text/javascript\">function tabClick(){var td=event.srcElement;while(td.localName!=\"td\"&&td.localName!=\"th\"){td=td.parentElement}var row=td.parentElement.rowIndex+1;var col=td.cellIndex+1;var value=td.innerHTML;var str='{\"row\":\"'+row+'\",\"col\":\"'+col+'\",\"value\":\"'+value+'\"}';if(window.TableViewer){window.TableViewer.onClick(row,col,value)}console.log(str)}function scrollTo(row,col,isSmooth){var element=document.getElementById(\"r\"+row+\"c\"+col);if(element){if(isSmooth){element.scrollIntoView({behavior:'smooth'})}else{element.scrollIntoView({})}}}function setStyle(style){document.querySelector(\"style\").innerText=style}function setData(data){document.body.innerHTML=data}var lastVisiblePage=\"r1c1\";window.onscroll=function printVisible(){var visibleId=\"r1c1\";var cells=document.querySelectorAll(\"td\");for(var i=0;i<cells.length;i++){var rect=cells[i].getBoundingClientRect();if(rect.x>=-1&&rect.y>=-1){visibleId=cells[i].id;break}}if(lastVisiblePage!=visibleId){lastVisiblePage=visibleId;var temp=visibleId.replace(\"r\",\"\").split(\"c\");if(window.TableViewer){window.TableViewer.onScroll(temp[0],temp[1])}console.log(temp[0],temp[1])}};</script>";
	private String linkSheet = "";
	private String part3 = "<style type=\"text/css\">";
	private String styleString = "";
	private String part5 = "</style></head><body>";
	private String dataString = "";
	private String jsString = "";
	private String part7 = "</body></html>";
	private String sourcecode = part1 + part2 + linkSheet + part3 + styleString + part5 + dataString + jsString + part7;

	public TableViewer(ComponentContainer container) {
		super(container.$form());
		this.context = container.$context();
		activity = container.$context();
		basicStyle();
	}

	@SuppressLint({ "SetJavaScriptEnabled" })
	@SimpleFunction(description = "initialize this tableviewer")
	public void Init(HVArrangement layout) {
		container = layout;
		realInit();
	}

	private void realInit() {
		ViewGroup viewGroup = (ViewGroup) container.getView();
		viewGroup.removeAllViews();
		this.webView = new WebView(this.context);
		viewGroup.addView(this.webView);

		WebSettings webSettings = this.webView.getSettings();
		webSettings.setJavaScriptEnabled(true);
		webSettings.setSupportZoom(true);
		webSettings.setBuiltInZoomControls(true);
		webSettings.setDisplayZoomControls(false);
		webSettings.setAllowFileAccess(true);
		webView.addJavascriptInterface(new JsObject(), "TableViewer");
		webView.setWebViewClient(webViewClient);
		Log.i(TAG, "Init");

		if (transparentbg) {
			webView.setBackgroundColor(0);
			Drawable background = webView.getBackground();
			if (background != null) {
				background.setAlpha(0);
			}
		}
	}

	WebViewClient webViewClient = new WebViewClient() {
		@Override
		public void onPageFinished(WebView view, String url) {
			Log.i(TAG, "onPageFinished");
			String js = buildClassString();
			js += "window.TableViewer.onSetClass(document.documentElement.outerHTML); ";
			Log.i(TAG, js);
			webView.evaluateJavascript(js, null);

			activity.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					OnShowTable();
				}
			});

		}
	};

	@SimpleFunction(description = "data: list of list")
	public void SetData(YailList data) {
		try {
			JSONArray arr = new JSONArray(data.toJSONString());
			DATAS = new ArrayList<ArrayList<String>>();
			for (int i = 0; i < arr.length(); i++) {
				ArrayList<String> rowList = new ArrayList<String>();
				JSONArray row = arr.getJSONArray(i);

				for (int j = 0; j < row.length(); j++) {
					rowList.add(row.getString(j));
				}
				DATAS.add(rowList);
			}
		} catch (JSONException e) {
			e.printStackTrace();
		}

	}

	@SimpleFunction(description = "Setting value to empty text will remove this style. You can write your own class here")
	public void SetStyle(String selector, String attribute, String value) {
		addStyle(selector, attribute, value);
	}

	@SimpleFunction(description = "Write you own stylesheet here, or import external stylesheet like 'http://...' or 'file://...'")
	public void SetStyleSheet(String sheet) {
		if (sheet.toLowerCase().startsWith("http://") || sheet.toLowerCase().startsWith("https://")) {
			this.linkSheet += " <link rel=\"stylesheet\" href=\"" + sheet + "\">";
		} else if (sheet.toLowerCase().endsWith(".css")) {
			sheet = getFullPath(sheet);
			this.linkSheet += " <link rel=\"stylesheet\" href=\"" + sheet + "\">";
		} else {
			this.styleSheet += " " + sheet;
		}
	}

	private void addStyle(String selector, String attribute, String value) {
		HashMap<String, String> style;
		if (STYLES.containsKey(selector)) {
			style = STYLES.get(selector);
		} else {
			style = new HashMap<String, String>();
		}
		style.put(attribute, value);
		STYLES.put(selector, style);
	}

	@SimpleFunction(description = "")
	public void SetClass(String selector, String className) {
		if (className.equals("")) {
			CLASSES.remove(selector.toLowerCase());
		} else {
			CLASSES.put(selector.toLowerCase(), className);
		}
	}

	@SimpleFunction(description = "")
	public void SetJavascript(String js) {
		jsString = "<script type=\"text/javascript\">" + js + "</script>";
	}

	@SimpleFunction(description = "clear all styles and data")
	public void ClearTable() {
		DATAS = new ArrayList<ArrayList<String>>();
		ClearStyle();
		basicStyle();
		ShowTable();
	}

	@SimpleFunction(description = "clear all styles, including the basic style (showing the table border)")
	public void ClearStyle() {
		STYLES = new LinkedHashMap<String, HashMap<String, String>>();
		styleSheet = "";
		linkSheet = "";
		CLASSES = new HashMap<>();
	}

	@SimpleFunction
	public void ShowTable() {
		if (webView == null) {
			throw new YailRuntimeError("TableViewer not initialized", "ShowTable");
		}
		buildDataString();
		buildStyleString();
		sourcecode = part1 + part2 + linkSheet + part3 + styleString + part5 + dataString + jsString + part7;
		webView.loadDataWithBaseURL("", sourcecode, "text/html", "utf-8", null);
		isShowTable = true;
	}

	@SimpleFunction(description = "smooth: boolean. Not working if container/layout height set to automatic")
	public void ScrollTo(int row, int col, boolean smooth) {
		if (isShowTable) {
			webView.evaluateJavascript("scrollTo(" + row + "," + col + "," + smooth + ")", null);
		} else {
			throw new YailRuntimeError("ShowTable needed", "ScrollTo");
		}

	}

	@SimpleFunction
	public void SetStyleWithJs(String selector, String attribute, String value) {
		if (isShowTable) {
			String js = "document.querySelectorAll('" + selector + "').forEach(ele=>{ele.style = '" + attribute + ":"
					+ value + "'});";
			js += "window.TableViewer.onSetClass(document.documentElement.outerHTML); ";
			webView.evaluateJavascript(js, null);

		} else {
			throw new YailRuntimeError("ShowTable needed", "SetStyleWithJs");
		}
	}

	@SimpleFunction(description = "")
	public void RunJs(String js) {
		if (isShowTable) {
			webView.evaluateJavascript(js, new ValueCallback<String>() {
				@Override
				public void onReceiveValue(String arg0) {
					AfterRunJs(arg0);
				}
			});
		} else {
			throw new YailRuntimeError("ShowTable needed", "RunJs");
		}

	}

	@SimpleEvent
	public void AfterRunJs(String result) {
		EventDispatcher.dispatchEvent(this, "AfterRunJs", result);
	}

	@SimpleProperty
	public String SourceCode() {
		return sourcecode;
	}

	@SimpleProperty(description = "")
	public void CellPadding(String value) {
		if (!value.endsWith("px"))
			value += "px";
		addStyle("th,td", "padding", value);
	}

	@SimpleProperty(description = "")
	public void HeadBgColor(String value) {
		addStyle("thead th", "background-color", value);
	}

	@SimpleProperty(description = "")
	public void FootBgColor(String value) {
		addStyle("tfoot tr", "background-color", value);
	}

	@SimpleProperty(description = "")
	public void BorderColor(String value) {
		addStyle("th,td", "border-color", value);
	}

	@SimpleProperty(description = "")
	public void HasHead(boolean head) {
		this.hasHead = head;
	}

	@SimpleProperty(description = "")
	public void HasFoot(boolean foot) {
		this.hasFoot = foot;
	}

	@SimpleProperty(description = "")
	public void BorderWidth(String value) {
		if (!value.endsWith("px"))
			value += "px";
		addStyle("th,td", "border-width", value);
	}

	@SimpleProperty(description = "")
	public void FontSize(String value) {
		if (!value.endsWith("px"))
			value += "px";
		addStyle("th,td", "font-size", value);
	}

	@DesignerProperty(editorType = "boolean", defaultValue = "False")
	@SimpleProperty(userVisible = false)
	public void TransparentBackground(boolean transp) {
		transparentbg = transp;

	}

	private void basicStyle() {
		addStyle("table", "border-collapse", "collapse");
		addStyle("table", "width", "100%");
		addStyle("th", "border", "1px solid black");
		addStyle("th", "text-align", "center");
		addStyle("th", "padding", "5px");
		addStyle("td", "border", "1px solid black");
		addStyle("td", "text-align", "center");
		addStyle("td", "padding", "5px");
	}

	private String buildClassString() {
		StringBuilder result = new StringBuilder();

		for (String key : CLASSES.keySet()) {
			result.append("document.querySelectorAll(\"").append(key).append("\").forEach(ele=>ele.className=\"")
					.append(CLASSES.get(key)).append("\"); ");
		}
		return result.toString();

	}

	private void buildStyleString() {
		StringBuilder result = new StringBuilder();

		for (String key : STYLES.keySet()) {
			result.append(key).append(" {");

			HashMap<String, String> style = STYLES.get(key);

			for (String key2 : style.keySet()) {
				if (!style.get(key2).equals("")) {
					result.append(key2).append(":").append(style.get(key2)).append(";");
				}
			}
			result.append("} ");
		}
		styleString = result.toString();
		styleString += styleSheet;
	}

	private void buildDataString() {

		StringBuilder result = new StringBuilder();
		result.append("<table onclick='tabClick();' id=table1>");

		if (hasHead & DATAS.size() > 0) {
			result.append("<thead>");
			result.append("<tr id=tr1>");
			for (int j = 0; j < DATAS.get(0).size(); j++) {
				result.append("<th id=r").append(1).append("c").append(j + 1);
				result.append(">").append(DATAS.get(0).get(j)).append("</th>");

			}
			result.append("</tr>");
			result.append("</thead>");
		}

		result.append("<tbody>");
		for (int i = (hasHead ? 1 : 0); i < (hasFoot ? DATAS.size() - 1 : DATAS.size()); i++) {
			result.append("<tr id=tr").append(i + 1).append(">");
			ArrayList<String> row = DATAS.get(i);
			for (int j = 0; j < row.size(); j++) {
				result.append("<td id=r").append(i + 1).append("c").append(j + 1);
				result.append(">").append(row.get(j)).append("</td>");
			}
			result.append("</tr>");
		}
		result.append("</tbody>");

		if (hasFoot & DATAS.size() > 1) {
			result.append("<tfoot>");
			result.append("<tr id=tr").append(DATAS.size()).append(">");
			for (int j = 0; j < DATAS.get(DATAS.size() - 1).size(); j++) {
				result.append("<td id=r").append(DATAS.size()).append("c").append(j + 1);
				result.append(">").append(DATAS.get(DATAS.size() - 1).get(j)).append("</td>");
			}
			result.append("</tr>");
			result.append("</tfoot>");
		}
		result.append("</table>");
		dataString = result.toString();

	}

	@SimpleEvent
	public void Click(String row, String col, String value) {
		EventDispatcher.dispatchEvent(this, "Click", row, col, value);
	}

	@SimpleEvent(description = "Not working if container/layout height set to automatic")
	public void OnScroll(String row, String col) {
		EventDispatcher.dispatchEvent(this, "OnScroll", row, col);
	}

	@SimpleEvent(description = "fired after ShowTable function called")
	public void OnShowTable() {
		EventDispatcher.dispatchEvent(this, "OnShowTable");
	}

	private class JsObject {
		private JsObject() {
		}

		@JavascriptInterface
		public void onClick(final String row, final String col, final String value) {
			activity.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					Click(row, col, value);
				}
			});
		}

		@JavascriptInterface
		public void onScroll(final String row, final String col) {
//			TableViewer.this.visibleCol = Integer.parseInt(col);
//			TableViewer.this.visibleRow = Integer.parseInt(row);
			activity.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					OnScroll(row, col);
				}
			});
		}

		@JavascriptInterface
		public void onSetClass(final String html) {
			sourcecode = html;
		}
	}

	private String getFullPath(String filename) {
		filename = filename.replaceAll("file://", "");
		String result = "";

		if (filename.startsWith("//")) {
			filename = filename.substring(2);
			if (ReplForm.class.isInstance(this.form)) {
				String asd = this.context.getExternalFilesDir(null).getPath();
				if (Build.VERSION.SDK_INT >= 29) {
					result = asd + "/assets/" + filename;
				} else {
					result = asd + "/AppInventor/assets/" + filename;
				}
			} else {
				result = this.context.getFilesDir().getPath() + "/" + filename;
				try {
					InputStream in = this.form.openAsset(filename);
					FileUtil.writeStreamToFile(in, result);
				} catch (IOException localIOException) {

				}
			}
		} else if (filename.startsWith("/storage/emulated/")) {
			result = filename;
		} else if (filename.startsWith("/")) {
			result = this.context.getExternalFilesDir(null).getPath() + filename;
		} else {
			result = this.context.getExternalFilesDir(null).getPath() + "/" + filename;
		}
		return "file://" + result;
	}
}
